package com.web_bloks.document.client;

import com.web_bloks.system.client.Logger;
import com.web_bloks.system.client.Time;

import com.google.gwt.gears.client.Factory;
import com.google.gwt.gears.client.database.Database;
import com.google.gwt.gears.client.database.DatabaseException;
import com.google.gwt.gears.client.database.ResultSet;

import com.web_bloks.types.client.SimpleMap;

/**
 * GearsDocumentStore
 * 
 * @author Jean-Lou Dupont
 *
 */
public class GearsDocumentStore 
	extends BaseDocumentStore {
	
	public final static int DEFAULT_TTL = 24*60*60*1000;
	public final static long DEFAULT_CACHE_TIMEOUT = 24*60*60*1000; // 1day in milli
	
	final static String TABLE_NAME = "local_page_store";
	
	/**
	 * Follows SQLite datatypes
	 * 
	 * name:  	Unique Name(within ns)	TEXT
	 * nsid:	Unique namespace		TEXT
	 * content:	content data			BLOB
	 * type:  	HTTP Content-Type		TEXT
	 * cache: 	HTTP Cache-Control		TEXT
	 * expires: HTTP Expires			TEXT
	 * modified:HTTP Last-Modified		TEXT
	 * 
	 */
	final static String thisSchema = "(nsid TEXT, name TEXT, content BLOB, meta TEXT, expires TEXT, modified TEXT, etag TEXT, created INTEGER, ttl INTEGER )";
	//                                 <------------ PageData -------->  <-- MetaData --------------------------------------> <------ protection ---------->
	
	/**
	 * Storage name
	 *  i.e. database name
	 */
	String storageName = null;
	
	/**
	 * Database instance
	 */
	Database db = null;
	
	/*===================================================================
	 * CONSTRUCTORS 
	 ===================================================================*/
	public GearsDocumentStore(String storageName) {
		super();
		this.storageName = storageName;
	}
	
	/*===================================================================
	 *  
	 ===================================================================*/

	/**
	 */
	public void put(Document obj) throws DocumentStoreException {
		
		//PageData
		Title title		= obj.getPageTitle();
		String content	= obj.getPageContent();
		//MetaData
		String meta     = obj.serializeMeta();
		String expires	= obj.getMeta("Expires");
		String modified	= obj.getMeta("Last-Modified");
		String etag		= obj.getMeta("Etag");
		
		long created = Time.getTime();
		long ttl = DEFAULT_CACHE_TIMEOUT;
		
		ResultSet result = null;

		if ( null == title) {
			Logger.logWarn(getClass()+" storage:["+storageName+"]"+".put: title can't be null");
			throw new DocumentStoreException( getClass()+" storage:["+storageName+"]"+".put: title cannot be null" );
		}
		String titleText    = title.toString();
		String titleName	= title.getName();
		Namespace titleNs	= title.getNamespace();
		
		String nsId   = titleNs.toString();
		
		if (null == nsId) {
			Logger.logWarn(getClass()+" storage:["+storageName+"]"+".put: title namespace can't be null");
			throw new DocumentStoreException( getClass()+" storage:["+storageName+"]"+".put: title namespace can't be null" );
		}

		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".put: name["+titleText+"]");
		//Logger.logInfo(getClass()+" storage:["+storageName+"]"+".put: meta["+meta+"]");

		try {
			result = this.db.execute("INSERT OR REPLACE INTO "+TABLE_NAME+" (nsid, name, content, meta, expires, modified, created, etag, ttl) VALUES(?,?,?,?,?,?,?,?,?)", 
							new String[] {nsId, titleName,content,meta,expires,modified,String.valueOf(created),etag, String.valueOf(ttl) } );
			int lastrow = this.db.getLastInsertRowId();
			Logger.logInfo(getClass()+".put: result count: " + result.getFieldCount() +", lastrowid:" + lastrow);
			
		} catch(DatabaseException e) {
			Logger.logError(getClass()+" storage:["+storageName+"]"+".put: EXCEPTION raised");
			throw new DocumentStoreException( e.getMessage() );
		}
		//Logger.logInfo(getClass()+".put: rowsAffected " + this.db.rowsAffected );
		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".put: stored name["+titleText+"] { nsid["+nsId+"] name["+titleName+"] }");
	}
	/**
	 * 
	 */
	public Document get(String name) throws DocumentStoreException {
		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".get: name["+name+"]");
		
		if ( (null == name) || ( name.length() == 0 ) )
			throw new DocumentStoreException( getClass()+" storage:["+storageName+"]"+".get: key cannot be empty" );

		// make sure we have a valid Title
		Title title = null;
		Exception exception = null;
		try {
			title = Title.newFromText(name);
		} catch(TitleException e) {
			exception = e;
		} catch(NamespaceException e) {
			exception = e;
		} finally {
			if ( null != exception ) {
				Logger.logWarn(getClass()+".get: exception raised, msg="+exception.getMessage());
			}
		}
		if ( null == title )
			return null;

		DocumentObject obj  = DocumentObject.createFromTitle( title );
		
		ResultSet result = null;
		String mapString = null;
		SimpleMap metaMap;

		long created = -1;
		long ttl     = -1;
		
		boolean found = false;

		String titleName = title.getName();
		Namespace titleNs = title.getNamespace();
		String nsId = titleNs.toString();
		
		try {
			result = this.db.execute(	"SELECT content,meta,expires,modified,created,etag, ttl FROM "+TABLE_NAME+" WHERE nsid=? AND name=?", 
										new String[] {nsId, titleName} );
			
			//Logger.logDir(result);
			
			// we should only get one element (if any)
			// and that's what we assume
			if (result.isValidRow()) {
				
				obj.setPageContent(result.getFieldAsString(0));
				
				mapString = result.getFieldAsString(1);
				metaMap = SimpleMap.fromSerializedString(mapString);
				obj.setMetaFromSimpleMap(metaMap);
				
				// must be set AFTER creating setting the meta information
				// using 'setMetaFromSimpleMap'
				obj.setMeta("Expires",result.getFieldAsString(2));
				obj.setMeta("Last-Modified",result.getFieldAsString(3));
				
				created = result.getFieldAsLong(4);
				obj.setMeta("Etag", result.getFieldAsString(5) );
				
				ttl = result.getFieldAsLong(6);
				
				found = true;
				
			} else {
				Logger.logWarn(getClass()+" storage:["+storageName+"]"+".get: row is not valid");
			}

			result.close();
			
		} catch(DatabaseException e) {
			Logger.logError(getClass()+" storage:["+storageName+"]"+".get: DatabaseException raised whilst SELECTing. Msg= " +e.getMessage());
			throw new DocumentStoreException( e.getMessage() );
		} catch( Exception e ) {
			Logger.logError(getClass()+"s torage:["+storageName+"]"+".get: Exception raised whilst SELECTing. Msg= " +e.getMessage());
			throw new DocumentStoreException( e.getMessage() );
		}

		if (!found) {
			Logger.logInfo(getClass()+" storage:["+storageName+"]"+".get: name["+name+"] not found");
			return null;
		}
		Logger.logInfo(getClass()+".get: got ==> nsid["+nsId+"] name["+titleName+"]");
		
		// check expiry
		long currentTime = Time.getTime();
		long delta = currentTime - created;
		long expires;
		String expiresMeta = obj.getMeta("Expires");
		if (expiresMeta.length() == 0) {
			expires = 0;
		} else {
			expires = Time.parse( expiresMeta );
		}
		
		// expired?
		if ( expires !=0 ) {
			if ( expires < currentTime ) {
				Logger.logWarn(getClass()+" storage:["+storageName+"]"+".get: document expired, name=["+name+"]" );
				delete(name);
				return null;
			}
		}
		
		if (  delta > ttl ) {
				Logger.logWarn(getClass()+" storage:["+storageName+"]"+".get: document expired (protection TTL), name=["+name+"]" );
				delete(name);
				return null;
		}
		
		//everything is fine... set the document's status
		obj.setState(Document.STATE_VALID);
		
		//Logger.log(getClass()+" storage:["+storageName+"]"+".get: got name["+name+"]");
		Logger.log(getClass()+" storage:["+storageName+"]"+".get: got meta["+mapString+"]");
		return obj;
	}
	/**
	 * Delete a document
	 * @param
	 */
	public void delete(String titleText) throws DocumentStoreException {
		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".delete: name["+titleText+"]");
		
		if ( (null == titleText) || ( titleText.length() == 0 ) )
			throw new DocumentStoreException( getClass()+" storage:["+storageName+"]"+".delete: name cannot be null" );

		Title title = createTitle(titleText);
		if (null == title)
			return;
		
		String name = title.getName();
		String nsid = title.getNamespace().toString();
		
		try {
			this.db.execute(	"DELETE FROM "+TABLE_NAME+" WHERE nsid=? AND name=?", new String[] {nsid,name} );
		} catch(DatabaseException e) {
			throw new DocumentStoreException( e.getMessage() );
		} 
	}
	public void clearNamespace(Namespace ns) throws DocumentStoreException {
		
		if ( (null == ns) || (null == ns.toString()) ) {
			throw new DocumentStoreException(getClass()+".clearNamespace: invalid namespace");
		}

		String nsid = ns.toString();
		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".clearNamespace, nsid["+nsid+"]");		
		
		try {
			this.db.execute(	"DELETE FROM localdocumentstore4 WHERE nsid=?", new String[] {nsid} );
		} catch(DatabaseException e) {
			throw new DocumentStoreException( e.getMessage() );
		}
	}
	
	/**
	 * Vaccums the datastore 
	 */
	public void clear() throws DocumentStoreException {
		Logger.logWarn( getClass()+"::clear" );
		
		try {
			this.db.execute("DELETE FROM "+TABLE_NAME );
		} catch(DatabaseException e) {
			throw new DocumentStoreException( e.getMessage() );
		}
	}
	
	public boolean exists() {
		boolean r = isGearsInstalled();
		return r;
	}

	public boolean isPersistent() {
		return true;
	}

	public boolean initialize() throws DocumentStoreException {
		
		if (!isGearsInstalled())
			return false;
		
		//Logger.logInfo( getClass()+"::initialize" );
		
		if (this.db != null)
			return false;
		
		if (this.storageName.length() == 0) {
			throw new DocumentStoreException("GearsDocumentStore.initialize: database name cannot be null");
		}
		
		return createDatabase();
	}
	
	/*===================================================================
	 * PROTECTED 
	 ===================================================================*/
	protected Title createTitle(String titleText) {
		
		Title title = null;
		Exception e = null;
		try {
			title = Title.newFromText(titleText);
		} catch(TitleException te) {
			e = te;
		} catch(NamespaceException ne) {
			e = ne;
		}
		if (null != e) {
			Logger.logError(getClass()+".createTitle: exception raised, msg=" + e.getMessage());
		}
		return title;
	}
	
	protected boolean createDatabase() throws DocumentStoreException {
		//Logger.logInfo( getClass()+"::createDatabase" );
		
		Factory factory = Factory.getInstance();
		
		if (null==factory) {
			Logger.logInfo( getClass()+".createDatabase: can't get Factory... maybe user denied access to Gears" );
			return false;
		}
		try {		
			this.db = factory.createDatabase();
			this.db.open( this.storageName );
			this.db.execute("create table if not exists "+TABLE_NAME+" "+thisSchema);
			
		} catch(DatabaseException e) {
			Logger.logError(getClass()+" storage:["+storageName+"]"+"::createDatabase: exception raised whilst creating table. Msg= " + e.getMessage());
			try {
				Logger.logError(getClass()+" storage:["+storageName+"]"+"::createDatabase: dropping table. Msg= " + e.getMessage());
				this.db.execute("drop table localdocumentstore4");
			} catch(Exception e2) {
				Logger.logError(getClass()+" storage:["+storageName+"]"+"::createDatabase: dropping table raised exception. Msg= " + e2.getMessage());
			}
			throw new DocumentStoreException( e.getMessage() );
		}
		Logger.logInfo(getClass()+" storage:["+storageName+"]"+".createDatabase, created database name["+this.storageName+"]");	
		
		return true;
	}
	
	/*===================================================================
	 * PRIVATE 
	 ===================================================================*/
	private static native boolean isGearsInstalled() /*-{
		try {
			return $wnd.google.gears.factory != null;
		} catch (e) {
			return false;
		}
	}-*/;
	
}//END